
/*-------------------------------------------------------------------------
 *
 * snapbuild.c
 *
 *	  基于WAL内容构建历史目录快照的基础设施，
 *	  目的是解码WAL中的heapam.c风格值。
 *
 * NOTES:
 *
 * 我们构建的快照只能用于读取目录内容， 
 * 我们通过读取和解释WAL流来实现。目的是构建一个快照，
 * 其行为与生成XLogRecord时新拍摄的MVCC快照相同。
 *
 * 为了构建快照，我们重用了为热备份建立的基础设施。
 * 我们构建的内存快照与HS的不同，因为我们有不同的需求。
 * 为了成功解码来自WAL的数据，我们只需访问目录表和 
 * （sys|rel|cat）缓存，不需要访问实际的用户表，因为我们
 * 解码的数据完全包含在WAL记录中。此外，与正常MVCC快照
 * 相比，我们的快照需要有所不同，因为与那些快照相比，我们
 * 不能完全依赖clog和pg_subtrans来获取有关已提交事务的信息，
 * 因为这些事务可能会在我们当前解码的WAL条目的视角下未来
 * 提交。这一定义的好处在于，我们只需防止目录行的删除，
 * 而正常表的行仍然可以被删除。这是通过使用复制槽机制实现的。
 *
 * 通常，修改目录的事务在与仅操作用户数据的事务相比所占的
 * 比例相当小，我们在 [xmin, xmax) 中跟踪已提交的修改目录的
 * 事务，而不是像正常快照中那样跟踪所有正在运行的事务。
 * 注意，我们通常只关注已获取xid的事务。也就是说，我们
 * 在 snapshot->(xmin, xmax) 之间维护一个认为已提交的事务列表，
 * 其他一切都被认为是中止或进行中的。这也使我们不必担心
 * 在提交之前的子事务，意味着该模块与HS相比，不必关心
 * 子溢出的子事务及其相似问题。
 *
 * 做到这一点的一个复杂性是，为了处理混合DDL/DML事务，
 * 我们需要在事务中查看目录的中间版本的快照。在正常操作中
 * 这是通过使用CommandIds/cmin/cmax实现的。然而，问题在于
 * 由于空间效率原因，仅存储一个值
 * （参见 combocid.c）。由于组合CID仅在内存中可用，我们记录
 * 额外的信息，使我们能够在可见性检查期间获取原始的
 * （cmin，cmax）对。有关详细信息，请查看reorderbuffer.c中
 * 关于ResolveCminCmaxDuringDecoding()的注释。
 *
 * 为了便利所有这些，我们需要自己的可见性例程，因为
 * 正常的可见性例程针对不同的用例进行了优化。
 *
 * 要用解码快照替换正常的目录快照，请使用
 * SetupHistoricSnapshot() 和 TeardownHistoricSnapshot() 函数。
 *
 *
 *
 * snapbuild机制分阶段启动，如下图所示
 * 描述SnapBuild->状态转换：
 *
 *		   +-------------------------+
 *	  +----|		 START			 |-------------+
 *	  |    +-------------------------+			   |
 *	  |					|						   |
 *	  |					|						   |
 *	  |		   running_xacts #1					   |
 *	  |					|						   |
 *	  |					|						   |
 *	  |					v						   |
 *	  |    +-------------------------+			   v
 *	  |    |   BUILDING_SNAPSHOT	 |------------>|
 *	  |    +-------------------------+			   |
 *	  |					|						   |
 *	  |					|						   |
 *	  | running_xacts #2, xacts from #1 finished   |
 *	  |					|						   |
 *	  |					|						   |
 *	  |					v						   |
 *	  |    +-------------------------+			   v
 *	  |    |	   FULL_SNAPSHOT	 |------------>|
 *	  |    +-------------------------+			   |
 *	  |					|						   |
 * running_xacts		|					   saved snapshot
 * with zero xacts		|				  at running_xacts's lsn
 *	  |					|						   |
 *	  | running_xacts with xacts from #2 finished  |
 *	  |					|						   |
 *	  |					v						   |
 *	  |    +-------------------------+			   |
 *	  +--->|SNAPBUILD_CONSISTENT	 |<------------+
 *		   +-------------------------+
 *
 * 初始化时，机制处于START阶段。当读取到一个xl_running_xacts
 * 记录，该记录是相对较新的（在安全的xmin视野之上），
 * 就会发生状态转换。如果在运行的 xacts 生成时没有
 * 运行的 xacts，则我们将直接进入 CONSISTENT 状态，
 * 否则我们将切换到 BUILDING_SNAPSHOT 状态。拥有完整快照意味着
 * 从此开始的所有事务都可以完全解码，但之前开始的事务则不行。
 * 在 FULL_SNAPSHOT 状态下，一旦所有那些先前运行的事务
 * 已提交或中止，我们将切换到 CONSISTENT。
 *
 * 只有在达到 CONSISTENT 状态后提交的事务才会被重放，
 * 即使它们可能是在 FULL_SNAPSHOT 期间启动的。这确保我们
 * 达到一个没有以前更改被导出的点，但所有随后的更改
 * 都会被导出。这个点是初始化复制的便利点，因此我们
 * 在该点导出快照，该快照可以用于读取正常数据。
 *
 * Copyright (c) 2012-2022, PostgreSQL Global Development Group
 *
 * IDENTIFICATION
 *	  src/backend/replication/logical/snapbuild.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include <sys/stat.h>
#include <unistd.h>

#include "access/heapam_xlog.h"
#include "access/transam.h"
#include "access/xact.h"
#include "miscadmin.h"
#include "pgstat.h"
#include "replication/logical.h"
#include "replication/reorderbuffer.h"
#include "replication/snapbuild.h"
#include "storage/block.h"		/* 调试输出 */
#include "storage/fd.h"
#include "storage/lmgr.h"
#include "storage/proc.h"
#include "storage/procarray.h"
#include "storage/standby.h"
#include "utils/builtins.h"
#include "utils/memutils.h"
#include "utils/snapmgr.h"
#include "utils/snapshot.h"

/*
 * 该结构包含快照构建机制的当前状态。除了在头文件中的前向声明外，它不对外公开，因此我们可以轻松更改其内容。
 */
struct SnapBuild
{
	/* 我们在构建第一个完整快照方面进展如何 */
	SnapBuildState state;

	/* 用于为此模块分配内存的私有内存上下文。 */
	MemoryContext context;

	/* 所有事务 < 这个已提交/中止 */
	TransactionId xmin;

	/* 所有事务 >= 这个是未提交的 */
	TransactionId xmax;

	/*
	 * 不要重放 LSN < 这个 LSN 的提交。可以从外部设置，但它也将从 snapbuild.c 内部推进（永不回退）。
	 */
	XLogRecPtr	start_decoding_at;

	/*
	 * 启用两阶段解码的 LSN 或在创建槽时我们找到一致状态的 LSN。
	 *
	 * 因为之前没有启用两阶段或不在初始快照覆盖范围内而被跳过的准备好的事务，需要在稍后与提交的准备一起发送，并且它们必须在这个点之前。
	 */
	XLogRecPtr	two_phase_at;

	/*
	 * 在 "xl_running_xacts" 信息指示没有运行的 xid 小于这个值之前，不要开始解码 WAL。
	 */
	TransactionId initial_xmin_horizon;

	/* 指示我们正在构建完整快照还是只构建目录快照。 */
	bool		building_full_snapshot;

	/*
	 * 有效快照以查看此时看到的目录状态。
	 */
	Snapshot	snapshot;

	/*
	 * 我们确定快照已序列化到的最后位置的 LSN。
	 */
	XLogRecPtr	last_serialized_snapshot;

	/*
	 * 我们需要与可用快照等更新的重排缓冲区。
	 */
	ReorderBuffer *reorder;

	/*
	 * 下一个初始快照构建阶段将发生的 TransactionId。如果未知（即 SNAPBUILD_START），或当不需要下一个阶段时（SNAPBUILD_CONSISTENT），则为 InvalidTransactionId。
	 */
	TransactionId next_phase_at;

	/*
	 * 在 xmin 和 xmax 之间已提交的可能修改目录的事务数组。
	 */
	struct
	{
		/* 已提交事务的数量 */
		size_t		xcnt;

		/* 已提交事务的可用空间 */
		size_t		xcnt_space;

		/*
		 * 在我们达到 CONSISTENT 状态之前，我们记录所有事务的提交，而不仅仅是更改目录的事务。当这变化时记录下来，以便我们知道无法安全地导出快照。
		 */
		bool		includes_all_transactions;

		/*
		 * 修改了目录的已提交事务的数组。
		 *
		 * 由于此数组经常被修改，我们不以 xidComparator 顺序保存它。相反，我们在构建和分发快照时对数组进行排序。
		 *
		 * TODO：不清楚这种推理是否有多大意义。每次在变得一致后在这里添加内容也将需要分发快照。将它们存储为排序可能也会使清除更容易（但在环绕方面更复杂？）。如果在构建快照时排序出现在性能分析中，应进行改进。
		 */
		TransactionId *xip;
	}			committed;
};

/*
 * 开启一个事务——我们在导出快照时需要这样做——会移除关于之前使用的资源所有者的知识，因此我们在这里保存它。
 */
static ResourceOwner SavedResourceOwnerDuringExport = NULL;
static bool ExportInProgress = false;

/*
 * 当我们解码的 xl_running_xacts 记录被写入时，正在运行的事务和子事务的数组。该数组
 * 按照 xidComparator 顺序排序。当 xids 成为足够旧的时候，我们会从这个数组中移除它们，
 * 然后最终这个数组会变得为空。这个数组是在 builder->context 中分配的，因此它的生命周期
 * 与快照构建器相同。
 *
 * 我们通常依赖一些 WAL 记录类型，例如 HEAP2_NEW_CID，以了解事务是否更改了目录。但有可能
 * 逻辑解码仅解码了事务的提交记录，并在恢复先前序列化的快照时，这种情况下我们将错过将
 * xid 添加到快照中，最终以错误的快照查看目录。
 *
 * 现在为了避免上述问题，如果在 InitialRunningXacts 中列出的 xid 的 COMMIT 记录具有
 * XACT_XINFO_HAS_INVALS 标志，我们将标记顶级事务及其子事务为包含目录更改。
 *
 * 我们最终可能会将未更改目录的事务添加到快照中，因为我们无法通过检查 COMMIT 记录区分
 * 事务是否有目录更改。它没有关于哪个（子）事务有目录更改的信息，而 XACT_XINFO_HAS_INVALS
 * 也不一定表示事务有目录更改。但这不会是问题，因为我们在解码期间构建的快照仅用于读取
 * 系统目录。
 */
static TransactionId *InitialRunningXacts = NULL;
static int	NInitialRunningXacts = 0;

/* -> 已提交和 InitailRunningXacts 操作 */
static void fc_SnapBuildPurgeOlderTxn(SnapBuild *fc_builder);

/* 快照构建/操作/分发函数 */
static Snapshot fc_SnapBuildBuildSnapshot(SnapBuild *fc_builder);

static void fc_SnapBuildFreeSnapshot(Snapshot fc_snap);

static void fc_SnapBuildSnapIncRefcount(Snapshot fc_snap);

static void fc_SnapBuildDistributeNewCatalogSnapshot(SnapBuild *fc_builder, XLogRecPtr fc_lsn);

/* SnapBuildProcessRunningXacts 的 xlog 读取助手函数 */
static bool fc_SnapBuildFindSnapshot(SnapBuild *fc_builder, XLogRecPtr fc_lsn, xl_running_xacts *fc_running);
static void fc_SnapBuildWaitSnapshot(xl_running_xacts *fc_running, TransactionId fc_cutoff);

/* 序列化函数 */
static void fc_SnapBuildSerialize(SnapBuild *fc_builder, XLogRecPtr fc_lsn);
static bool fc_SnapBuildRestore(SnapBuild *fc_builder, XLogRecPtr fc_lsn);

/*
 * 用于清除正在运行的事务和子事务数组的内存上下文重置回调。
 */
static void fc_SnapBuildResetRunningXactsCallback(void *fc_arg)
{
	NInitialRunningXacts = 0;
	InitialRunningXacts = NULL;
}

/*
 * 分配一个新的快照构建器。
 *
 * xmin_horizon 是 xid >=，我们可以确定没有目录行被删除，start_lsn 是 LSN >=，
 * 我们希望重放提交。
 */
SnapBuild *
AllocateSnapshotBuilder(ReorderBuffer *fc_reorder,
						TransactionId fc_xmin_horizon,
						XLogRecPtr fc_start_lsn,
						bool fc_need_full_snapshot,
						XLogRecPtr fc_two_phase_at)
{
	MemoryContext fc_context;
	MemoryContext fc_oldcontext;
	SnapBuild  *fc_builder;
	MemoryContextCallback *fc_mcallback;

	/* 在自己的上下文中分配内存，以便更好地进行核算 */
	fc_context = AllocSetContextCreate(CurrentMemoryContext,
									"snapshot builder context",
									ALLOCSET_DEFAULT_SIZES);
	fc_oldcontext = MemoryContextSwitchTo(fc_context);

	fc_builder = palloc0(sizeof(SnapBuild));

	fc_builder->state = SNAPBUILD_START;
	fc_builder->context = fc_context;
	fc_builder->reorder = fc_reorder;
	/* 其他结构成员通过上面的 palloc0 进行零初始化 */

	fc_builder->committed.xcnt = 0;
	fc_builder->committed.xcnt_space = 128;	/* 任意数字 */
	fc_builder->committed.xip =
		palloc0(fc_builder->committed.xcnt_space * sizeof(TransactionId));
	fc_builder->committed.includes_all_transactions = true;

	fc_builder->initial_xmin_horizon = fc_xmin_horizon;
	fc_builder->start_decoding_at = fc_start_lsn;
	fc_builder->building_full_snapshot = fc_need_full_snapshot;
	fc_builder->two_phase_at = fc_two_phase_at;

	fc_mcallback = palloc0(sizeof(MemoryContextCallback));
	fc_mcallback->func = fc_SnapBuildResetRunningXactsCallback;
	MemoryContextRegisterResetCallback(CurrentMemoryContext, fc_mcallback);

	MemoryContextSwitchTo(fc_oldcontext);

	/* 初始运行事务数组必须为空。 */
	Assert(NInitialRunningXacts == 0 && InitialRunningXacts == NULL);

	return fc_builder;
}

/*
 * 释放一个快照构建器。
 */
void FreeSnapshotBuilder(SnapBuild *fc_builder)
{
	MemoryContext fc_context = fc_builder->context;

	/* 明确释放快照，包含一些错误检查 */
	if (fc_builder->snapshot != NULL)
	{
		SnapBuildSnapDecRefcount(fc_builder->snapshot);
		fc_builder->snapshot = NULL;
	}

	/* 其他资源通过内存上下文重置被释放 */
	MemoryContextDelete(fc_context);
}

/*
 * 释放一个之前由我们构建的未引用的快照。
 */
static void fc_SnapBuildFreeSnapshot(Snapshot fc_snap)
{
	/* 确保我们没有传递一个外部快照 */
	Assert(fc_snap->snapshot_type == SNAPSHOT_HISTORIC_MVCC);

	/* 确保没有人修改我们的快照 */
	Assert(fc_snap->curcid == FirstCommandId);
	Assert(!fc_snap->suboverflowed);
	Assert(!fc_snap->takenDuringRecovery);
	Assert(fc_snap->regd_count == 0);

	/* 稍微更可能，所以即使没有 c-asserts 也会被检查 */
	if (fc_snap->copied)
		elog(ERROR, "cannot free a copied snapshot");

	if (fc_snap->active_count)
		elog(ERROR, "cannot free an active snapshot");

	pfree(fc_snap);
}

/*
 * 我们在快照构建的哪个状态？
 */
SnapBuildState
SnapBuildCurrentState(SnapBuild *fc_builder)
{
	return fc_builder->state;
}

/*
 * 返回第一次启用双阶段解码的 LSN。
 */
XLogRecPtr SnapBuildGetTwoPhaseAt(SnapBuild *fc_builder)
{
	return fc_builder->two_phase_at;
}

/*
 * 设置启用双阶段解码的 LSN。
 */
void SnapBuildSetTwoPhaseAt(SnapBuild *fc_builder, XLogRecPtr fc_ptr)
{
	fc_builder->two_phase_at = fc_ptr;
}

/*
 * 是否应该对以 'ptr' 结尾的事务内容进行解码？
 */
bool SnapBuildXactNeedsSkip(SnapBuild *fc_builder, XLogRecPtr fc_ptr)
{
	return fc_ptr < fc_builder->start_decoding_at;
}

/*
 * 增加快照的引用计数。
 *
 * 这在将快照交给某个外部资源时或在将快照添加为 builder->snapshot 时使用。
 */
static void fc_SnapBuildSnapIncRefcount(Snapshot fc_snap)
{
	fc_snap->active_count++;
}

/*
 * 减少快照的引用计数，并在引用计数达到零时释放。
 *
 * 对外可见，以便已获得 IncRef' 快照的外部资源可以轻松调整其引用计数。
 */
void SnapBuildSnapDecRefcount(Snapshot fc_snap)
{
	/* 确保我们没有传递一个外部快照 */
	Assert(fc_snap->snapshot_type == SNAPSHOT_HISTORIC_MVCC);

	/* 确保没有人修改我们的快照 */
	Assert(fc_snap->curcid == FirstCommandId);
	Assert(!fc_snap->suboverflowed);
	Assert(!fc_snap->takenDuringRecovery);

	Assert(fc_snap->regd_count == 0);

	Assert(fc_snap->active_count > 0);

	/* 更有可能，因此即使没有 casserts 也会进行检查 */
	if (fc_snap->copied)
		elog(ERROR, "cannot free a copied snapshot");

	fc_snap->active_count--;
	if (fc_snap->active_count == 0)
		fc_SnapBuildFreeSnapshot(fc_snap);
}

/*
 * 基于当前已提交的目录修改事务构建新的快照。
 *
 * 正在进行的具有目录访问权限的事务*不*允许修改这些快照；它们必须复制它们并填充适当的 ->curcid 和 ->subxip/subxcnt 值。
 */
static Snapshot fc_SnapBuildBuildSnapshot(SnapBuild *fc_builder)
{
	Snapshot	fc_snapshot;
	Size		fc_ssize;

	Assert(fc_builder->state >= SNAPBUILD_FULL_SNAPSHOT);

	fc_ssize = sizeof(SnapshotData)
		+ sizeof(TransactionId) * fc_builder->committed.xcnt
		+ sizeof(TransactionId) * 1 /* 顶级 xid */ ;

	fc_snapshot = MemoryContextAllocZero(fc_builder->context, fc_ssize);

	fc_snapshot->snapshot_type = SNAPSHOT_HISTORIC_MVCC;

	/*
	 * 我们误用了 SnapshotData 的 xip 和 subxip 字段的原始含义，以使其更适合我们的需要。
	 *
	 * 在 'xip' 数组中，我们存储必须视为已提交的事务。由于我们只会查看已修改目录的事务中的元组，因此存储在 xmin 和 xmax 之间的少量事务更有效（通常没有这些事务）。
	 *
	 * 在已修改目录的事务中使用的快照还使用 'subxip' 数组来存储其顶级 xid 和所有子事务 xids，因此我们可以识别何时需要将不在 xip 中但仍需可见的行视为可见。只有在事务被复制到修改目录事务的上下文中时，subxip 才会被填充，因为我们在事务之间共享一个快照。只要事务没有修改目录，它就不需要将任何未提交的行视为可见，因此没有这些 xids 的必要。
	 *
	 * 两个数组都经过 qsort，以便我们可以在它们上使用 bsearch()。
	 */
	Assert(TransactionIdIsNormal(fc_builder->xmin));
	Assert(TransactionIdIsNormal(fc_builder->xmax));

	fc_snapshot->xmin = fc_builder->xmin;
	fc_snapshot->xmax = fc_builder->xmax;

	/* 存储所有被此快照视为已提交的事务 */
	fc_snapshot->xip =
		(TransactionId *) ((char *) fc_snapshot + sizeof(SnapshotData));
	fc_snapshot->xcnt = fc_builder->committed.xcnt;
	memcpy(fc_snapshot->xip,
		   fc_builder->committed.xip,
		   fc_builder->committed.xcnt * sizeof(TransactionId));

	/* 排序以便我们可以 bsearch() */
	qsort(fc_snapshot->xip, fc_snapshot->xcnt, sizeof(TransactionId), xidComparator);

	/*
	 * 最初，subxip 是空的，即这是一个供不修改目录的事务使用的快照。如果需要，将由 ReorderBufferCopySnap() 填充。
	 */
	fc_snapshot->subxcnt = 0;
	fc_snapshot->subxip = NULL;

	fc_snapshot->suboverflowed = false;
	fc_snapshot->takenDuringRecovery = false;
	fc_snapshot->copied = false;
	fc_snapshot->curcid = FirstCommandId;
	fc_snapshot->active_count = 0;
	fc_snapshot->regd_count = 0;
	fc_snapshot->snapXactCompletionCount = 0;

	return fc_snapshot;
}

/*
 * 构建初始槽快照并将其转换为 HeapTupleSatisfiesMVCC 理解的普通快照。
 *
 * 该快照将可直接在当前事务中使用或导出以供在不同事务中加载。
 */
Snapshot SnapBuildInitialSnapshot(SnapBuild *fc_builder)
{
	Snapshot	fc_snap;
	TransactionId fc_xid;
	TransactionId *fc_newxip;
	int			fc_newxcnt = 0;

	Assert(!FirstSnapshotSet);
	Assert(XactIsoLevel == XACT_REPEATABLE_READ);

	if (fc_builder->state != SNAPBUILD_CONSISTENT)
		elog(ERROR, "cannot build an initial slot snapshot before reaching a consistent state");

	if (!fc_builder->committed.includes_all_transactions)
		elog(ERROR, "cannot build an initial slot snapshot, not all transactions are monitored anymore");

	/* 以便我们不覆盖现有值 */
	if (TransactionIdIsValid(MyProc->xmin))
		elog(ERROR, "cannot build an initial slot snapshot when MyProc->xmin already is valid");

	fc_snap = fc_SnapBuildBuildSnapshot(fc_builder);

	/*
	 * 我们知道 snap->xmin 是活跃的，由逻辑 xmin 机制强制执行。由于这一点，我们无需锁定即可进行此操作，因为我们只是在更改自己的值。
	 */
#ifdef USE_ASSERT_CHECKING
	{
		TransactionId fc_safeXid;

		LWLockAcquire(ProcArrayLock, LW_SHARED);
		fc_safeXid = GetOldestSafeDecodingTransactionId(false);
		LWLockRelease(ProcArrayLock);

		Assert(TransactionIdPrecedesOrEquals(fc_safeXid, fc_snap->xmin));
	}
#endif

	MyProc->xmin = fc_snap->xmin;

	/* 在事务上下文中分配 */
	fc_newxip = (TransactionId *)
		palloc(sizeof(TransactionId) * GetMaxSnapshotXidCount());

	/*
	 * snapbuild.c 以“反转”的方式构建事务，这意味着它
	 * 将已提交的事务存储在 ->xip 中，而不是正在进行的事务。通过将所有未提交的事务标记为
	 * 正在进行的来构建经典快照。这可能代价高昂。
	 */
	for (fc_xid = fc_snap->xmin; NormalTransactionIdPrecedes(fc_xid, fc_snap->xmax);)
	{
		void	   *fc_test;

		/*
		 * 检查事务是否使用解码快照的
		 * ->xip 的含义进行了提交。
		 */
		fc_test = bsearch(&fc_xid, fc_snap->xip, fc_snap->xcnt,
					   sizeof(TransactionId), xidComparator);

		if (fc_test == NULL)
		{
			if (fc_newxcnt >= GetMaxSnapshotXidCount())
				ereport(ERROR,
						(errcode(ERRCODE_T_R_SERIALIZATION_FAILURE),
						 errmsg("initial slot snapshot too large")));

			fc_newxip[fc_newxcnt++] = fc_xid;
		}

		TransactionIdAdvance(fc_xid);
	}

	/* 根据需要调整剩余的快照字段 */
	fc_snap->snapshot_type = SNAPSHOT_MVCC;
	fc_snap->xcnt = fc_newxcnt;
	fc_snap->xip = fc_newxip;

	return fc_snap;
}

/*
 * 导出一个快照，以便它可以在另一个会话中通过 SET TRANSACTION
 * SNAPSHOT 进行设置。
 *
 * 为此，我们需要在当前后端开始一个事务，因为
 * 导入方检查源事务是否仍然打开，以确保 xmin 拓展没有自那时起进行。
 */
const char * SnapBuildExportSnapshot(SnapBuild *fc_builder)
{
	Snapshot	fc_snap;
	char	   *fc_snapname;

	if (IsTransactionOrTransactionBlock())
		elog(ERROR, "cannot export a snapshot from within a transaction");

	if (SavedResourceOwnerDuringExport)
		elog(ERROR, "can only export one snapshot at a time");

	SavedResourceOwnerDuringExport = CurrentResourceOwner;
	ExportInProgress = true;

	StartTransactionCommand();

	/* 似乎没有一个好的 API 来设置这些 */
	XactIsoLevel = XACT_REPEATABLE_READ;
	XactReadOnly = true;

	fc_snap = SnapBuildInitialSnapshot(fc_builder);

	/*
	 * 现在我们已经构建了一个普通快照，使其处于活动状态并使用
	 * 普通机制进行导出
	 */
	fc_snapname = ExportSnapshot(fc_snap);

	ereport(LOG,
			(errmsg_plural("exported logical decoding snapshot: \"%s\" with %u transaction ID",
						   "exported logical decoding snapshot: \"%s\" with %u transaction IDs",
						   fc_snap->xcnt,
						   fc_snapname, fc_snap->xcnt)));
	return fc_snapname;
}

/*
 * 确保存在快照，如果没有，则为当前事务构建一个。
 */
Snapshot SnapBuildGetOrBuildSnapshot(SnapBuild *fc_builder, TransactionId fc_xid)
{
	Assert(fc_builder->state == SNAPBUILD_CONSISTENT);

	/* 只有在没有预构建快照的情况下才构建新的快照 */
	if (fc_builder->snapshot == NULL)
	{
		fc_builder->snapshot = fc_SnapBuildBuildSnapshot(fc_builder);
		/* 增加快照构建器的引用计数 */
		fc_SnapBuildSnapIncRefcount(fc_builder->snapshot);
	}

	return fc_builder->snapshot;
}

/*
 * 如果有预先 SnapBuildExportSnapshot() 导出的快照，则重置该快照。
 * 中止先前启动的事务，并将资源所有者重置为其原始值。
 */
void SnapBuildClearExportedSnapshot(void)
{
	ResourceOwner fc_tmpResOwner;

	/* 没有任何导出，通常情况下就是这样 */
	if (!ExportInProgress)
		return;

	if (!IsTransactionState())
		elog(ERROR, "clearing exported snapshot in wrong transaction state");

	/*
	 * AbortCurrentTransaction() 负责重置快照状态，
	 * 所以记住 SavedResourceOwnerDuringExport。
	 */
	fc_tmpResOwner = SavedResourceOwnerDuringExport;

	/* 确保没有可能发生过的事情 */
	AbortCurrentTransaction();

	CurrentResourceOwner = fc_tmpResOwner;
}

/*
 * 在事务中止期间清除快照导出状态。
 */
void SnapBuildResetExportedSnapshotState(void)
{
	SavedResourceOwnerDuringExport = NULL;
	ExportInProgress = false;
}

/*
 * 处理单个堆变更的影响，适合当前快照构建器的状态，
 * 并返回在 (xid, lsn) 上所做的更改是否可以被解码。
 */
bool SnapBuildProcessChange(SnapBuild *fc_builder, TransactionId fc_xid, XLogRecPtr fc_lsn)
{
	/*
	 * 如果我们还没有构建快照，就无法处理事务中的数据，
	 * 所以不要存储它们。
	 */
	if (fc_builder->state < SNAPBUILD_FULL_SNAPSHOT)
		return false;

	/*
	 * 没有必要跟踪我们没有足够信息来解码的事务中的更改。
	 * 这意味着它们在我们进入 SNAPBUILD_FULL_SNAPSHOT 状态之前就开始了。
	 */
	if (fc_builder->state < SNAPBUILD_CONSISTENT &&
		TransactionIdPrecedes(fc_xid, fc_builder->next_phase_at))
		return false;

	/*
	 * 如果 reorderbuffer 还没有快照，现在添加一个，它将
	 * 在解码我们当前处理的更改时需要。
	 */
	if (!ReorderBufferXidHasBaseSnapshot(fc_builder->reorder, fc_xid))
	{
		/* 只有在没有预构建快照的情况下才构建新的快照 */
		if (fc_builder->snapshot == NULL)
		{
			fc_builder->snapshot = fc_SnapBuildBuildSnapshot(fc_builder);
			/* 增加快照构建器的引用计数 */
			fc_SnapBuildSnapIncRefcount(fc_builder->snapshot);
		}

		/*
		 * 增加我们正在处理快照的事务的引用计数。
		 */
		fc_SnapBuildSnapIncRefcount(fc_builder->snapshot);
		ReorderBufferSetBaseSnapshot(fc_builder->reorder, fc_xid, fc_lsn,
									 fc_builder->snapshot);
	}

	return true;
}

/*
 * 在读取 xl_heap_new_cid 记录后处理 CommandId/combo CID。
 * 这意味着某个事务对系统目录进行了某种形式的写入。
 */
void SnapBuildProcessNewCid(SnapBuild *fc_builder, TransactionId fc_xid,
					   XLogRecPtr fc_lsn, xl_heap_new_cid *fc_xlrec)
{
	CommandId	fc_cid;

	/*
	 * 只有在修改了目录元组时，我们才会记录 new_cid，所以将
	 * 事务标记为包含目录修改
	 */
	ReorderBufferXidSetCatalogChanges(fc_builder->reorder, fc_xid, fc_lsn);

	ReorderBufferAddNewTupleCids(fc_builder->reorder, fc_xlrec->top_xid, fc_lsn,
								 fc_xlrec->target_node, fc_xlrec->target_tid,
								 fc_xlrec->cmin, fc_xlrec->cmax,
								 fc_xlrec->combocid);

	/* 计算新命令 ID */
	if (fc_xlrec->cmin != InvalidCommandId &&
		fc_xlrec->cmax != InvalidCommandId)
		fc_cid = Max(fc_xlrec->cmin, fc_xlrec->cmax);
	else if (fc_xlrec->cmax != InvalidCommandId)
		fc_cid = fc_xlrec->cmax;
	else if (fc_xlrec->cmin != InvalidCommandId)
		fc_cid = fc_xlrec->cmin;
	else
	{
		fc_cid = InvalidCommandId; /* 使编译器静默 */
		elog(ERROR, "xl_heap_new_cid record without a valid CommandId");
	}

	ReorderBufferAddNewCommandId(fc_builder->reorder, fc_xid, fc_lsn, fc_cid + 1);
}

/*
 * 将一个新的快照添加到所有正在解码的事务中，这些事务
 * 当前正在进行中，以便它们可以看到刚刚提交的事务所做的新目录内容。
 * 这是必要的，因为这些正在进行中的事务将从此使用新的目录内容
 * （至少它们所做的一切需要与更新的目录内容兼容）。
 */
static void fc_SnapBuildDistributeNewCatalogSnapshot(SnapBuild *fc_builder, XLogRecPtr fc_lsn)
{
	dlist_iter	fc_txn_i;
	ReorderBufferTXN *fc_txn;

	/*
	 * 迭代所有顶层事务。这可以包括
	 * 子事务，我们只是在此之前不知道它们是那样，但这没关系，
	 * 它们只会被排队一个不必要的快照。
	 */
	dlist_foreach(fc_txn_i, &fc_builder->reorder->toplevel_by_lsn)
	{
		fc_txn = dlist_container(ReorderBufferTXN, node, fc_txn_i.cur);

		Assert(TransactionIdIsValid(fc_txn->xid));

		/*
		 * 如果我们还没有基快照，则此事务中没有更改，
		 * 这意味着我们还不需要快照。
		 * 当第一个更改被排队时，我们将添加一个快照。
		 *
		 * 注意：这对于子事务也能正确工作，因为
		 * ReorderBufferAssignChild() 确保将基快照转移到顶层事务，
		 * 并且在迭代更改队列时，我们会从子事务中获得更改。
		 */
		if (!ReorderBufferXidHasBaseSnapshot(fc_builder->reorder, fc_txn->xid))
			continue;

		/*
		 * 我们不需要将快照添加到准备好的事务中，因为它们
		 * 不应该看到新的目录内容。
		 */
		if (rbtxn_prepared(fc_txn) || rbtxn_skip_prepared(fc_txn))
			continue;

		elog(DEBUG2, "adding a new snapshot to %u at %X/%X",
			 fc_txn->xid, LSN_FORMAT_ARGS(fc_lsn));

		/*
		 * 增加快照的引用计数以便分配给我们正在处理的事务。
		 */
		fc_SnapBuildSnapIncRefcount(fc_builder->snapshot);
		ReorderBufferAddSnapshot(fc_builder->reorder, fc_txn->xid, fc_lsn,
								 fc_builder->snapshot);
	}
}

/*
 * 跟踪已提交的新目录更改事务。
 */
static void fc_SnapBuildAddCommittedTxn(SnapBuild *fc_builder, TransactionId fc_xid)
{
	Assert(TransactionIdIsValid(fc_xid));

	if (fc_builder->committed.xcnt == fc_builder->committed.xcnt_space)
	{
		fc_builder->committed.xcnt_space = fc_builder->committed.xcnt_space * 2 + 1;

		elog(DEBUG1, "increasing space for committed transactions to %u",
			 (uint32) fc_builder->committed.xcnt_space);

		fc_builder->committed.xip = repalloc(fc_builder->committed.xip,
										  fc_builder->committed.xcnt_space * sizeof(TransactionId));
	}

	/*
	 * TODO：在这里保持数组排序可能更有意义，而不是
	 * 每次我们构建新的快照时都进行排序。另一方面，这
	 * 在事务与子事务提交时会被重复调用。
	 */
	fc_builder->committed.xip[fc_builder->committed.xcnt++] = fc_xid;
}

/*
 * 删除我们视为已提交的事务的信息以及初始
 * 运行的事务，这些事务小于 ->xmin。它们将永远不会
 * 通过 ->committed 或 InitialRunningXacts 数组检查。
 * 已提交的 xids 将通过 clog 机制进行检查。
 *
 * 理想情况下，我们可以在事务完成（已提交/已中止）后
 * 从 InitialRunningXacts 数组中删除该事务，
 * 但这可能会很昂贵，因为我们需要维护数组中的 xids 顺序。
 */
static void fc_SnapBuildPurgeOlderTxn(SnapBuild *fc_builder)
{
	int			fc_off;
	TransactionId *fc_workspace;
	int			fc_surviving_xids = 0;

	/* 尚未准备好 */
	if (!TransactionIdIsNormal(fc_builder->xmin))
		return;

	/* TODO：是否有比简单复制和迭代更整洁的算法？ */
	fc_workspace =
		MemoryContextAlloc(fc_builder->context,
						   fc_builder->committed.xcnt * sizeof(TransactionId));

	/* 将仍然有趣的 xids 复制到工作区 */
	for (fc_off = 0; fc_off < fc_builder->committed.xcnt; fc_off++)
	{
		if (NormalTransactionIdPrecedes(fc_builder->committed.xip[fc_off],
										fc_builder->xmin))
			;					/* 删除 */
		else
			fc_workspace[fc_surviving_xids++] = fc_builder->committed.xip[fc_off];
	}

	/* 将工作区复制回持久状态 */
	memcpy(fc_builder->committed.xip, fc_workspace,
		   fc_surviving_xids * sizeof(TransactionId));

	elog(DEBUG3, "purged committed transactions from %u to %u, xmin: %u, xmax: %u",
		 (uint32) fc_builder->committed.xcnt, (uint32) fc_surviving_xids,
		 fc_builder->xmin, fc_builder->xmax);
	fc_builder->committed.xcnt = fc_surviving_xids;

	pfree(fc_workspace);

	/* 如果没有初始运行事务则快速退出 */
	if (NInitialRunningXacts == 0)
		return;

	/* 如果至少有一个事务要删除则进行边界检查 */
	if (!NormalTransactionIdPrecedes(InitialRunningXacts[0],
									 fc_builder->xmin))
		return;

	/*
	 * 也清除 InitialRunningXacts 中的 xids。清除的数组也必须
	 * 按 xidComparator 顺序进行排序。
	 */
	fc_workspace =
		MemoryContextAlloc(fc_builder->context,
						   NInitialRunningXacts * sizeof(TransactionId));
	fc_surviving_xids = 0;
	for (fc_off = 0; fc_off < NInitialRunningXacts; fc_off++)
	{
		if (NormalTransactionIdPrecedes(InitialRunningXacts[fc_off],
										fc_builder->xmin))
			;					/* 删除 */
		else
			fc_workspace[fc_surviving_xids++] = InitialRunningXacts[fc_off];
	}

	if (fc_surviving_xids > 0)
		memcpy(InitialRunningXacts, fc_workspace,
			   sizeof(TransactionId) * fc_surviving_xids);
	else
	{
		pfree(InitialRunningXacts);
		InitialRunningXacts = NULL;
	}

	elog(DEBUG3, "purged initial running transactions from %u to %u, oldest running xid %u",
		 (uint32) NInitialRunningXacts,
		 (uint32) fc_surviving_xids,
		 fc_builder->xmin);

	NInitialRunningXacts = fc_surviving_xids;
	pfree(fc_workspace);
}

/*
 * 处理事务提交时需要完成的所有工作
 */
void SnapBuildCommitTxn(SnapBuild *fc_builder, XLogRecPtr fc_lsn, TransactionId fc_xid,
				   int fc_nsubxacts, TransactionId *fc_subxacts)
{
	int			fc_nxact;

	bool		fc_needs_snapshot = false;
	bool		fc_needs_timetravel = false;
	bool		fc_sub_needs_timetravel = false;

	TransactionId fc_xmax = fc_xid;

	/*
	 * 在 BUILDING_SNAPSHOT 之前的事务将既不会被解码，
	 * 也不会成为快照的一部分。所以我们不需要记录任何东西。
	 */
	if (fc_builder->state == SNAPBUILD_START ||
		(fc_builder->state == SNAPBUILD_BUILDING_SNAPSHOT &&
		 TransactionIdPrecedes(fc_xid, fc_builder->next_phase_at)))
	{
		/* 确保只有在此之后的提交正在被重放 */
		if (fc_builder->start_decoding_at <= fc_lsn)
			fc_builder->start_decoding_at = fc_lsn + 1;
		return;
	}

	if (fc_builder->state < SNAPBUILD_CONSISTENT)
	{
		/* 确保只有在此之后的提交正在被重放 */
		if (fc_builder->start_decoding_at <= fc_lsn)
			fc_builder->start_decoding_at = fc_lsn + 1;

		/*
		 * 如果正在构建一个可导出的快照，强制跟踪 xid，即使
		 * 事务没有修改目录。
		 */
		if (fc_builder->building_full_snapshot)
		{
			fc_needs_timetravel = true;
		}
	}

	for (fc_nxact = 0; fc_nxact < fc_nsubxacts; fc_nxact++)
	{
		TransactionId fc_subxid = fc_subxacts[fc_nxact];

		/*
		 * 如果修改了目录，将子事务添加到基础快照中，我们不
		 * 在这里区分顶层事务。
		 */
		if (ReorderBufferXidHasCatalogChanges(fc_builder->reorder, fc_subxid))
		{
			fc_sub_needs_timetravel = true;
			fc_needs_snapshot = true;

			elog(DEBUG1, "found subtransaction %u:%u with catalog changes",
				 fc_xid, fc_subxid);

			fc_SnapBuildAddCommittedTxn(fc_builder, fc_subxid);

			if (NormalTransactionIdFollows(fc_subxid, fc_xmax))
				fc_xmax = fc_subxid;
		}

		/*
		 * 如果我们正在强制时间旅行，我们还需要子事务的可见性信息，
		 * 因此需要跟踪子事务的状态，即使它不修改目录。在这种情况下
		 * 不需要分发快照。
		 */
		else if (fc_needs_timetravel)
		{
			fc_SnapBuildAddCommittedTxn(fc_builder, fc_subxid);
			if (NormalTransactionIdFollows(fc_subxid, fc_xmax))
				fc_xmax = fc_subxid;
		}
	}

	/* 如果顶层修改了目录，它将需要一个快照 */
	if (ReorderBufferXidHasCatalogChanges(fc_builder->reorder, fc_xid))
	{
		elog(DEBUG2, "found top level transaction %u, with catalog changes",
			 fc_xid);
		fc_needs_snapshot = true;
		fc_needs_timetravel = true;
		fc_SnapBuildAddCommittedTxn(fc_builder, fc_xid);
	}
	else if (fc_sub_needs_timetravel)
	{
		/* 也跟踪顶层事务，子事务单独没有意义 */
		elog(DEBUG2, "forced transaction %u to do timetravel due to one of its subtransactions",
			 fc_xid);
		fc_needs_timetravel = true;
		fc_SnapBuildAddCommittedTxn(fc_builder, fc_xid);
	}
	else if (fc_needs_timetravel)
	{
		elog(DEBUG2, "forced transaction %u to do timetravel", fc_xid);

		fc_SnapBuildAddCommittedTxn(fc_builder, fc_xid);
	}

	if (!fc_needs_timetravel)
	{
		/* 记录我们无法再导出一般快照 */
		fc_builder->committed.includes_all_transactions = false;
	}

	Assert(!fc_needs_snapshot || fc_needs_timetravel);

	/*
	 * 调整快照生成器的 xmax，我们仅对已提交的、
	 * 修改目录的事务执行此操作，其他一切对我们来说都不重要，
	 * 因为我们不会查看相应的行。
	 */
	if (fc_needs_timetravel &&
		(!TransactionIdIsValid(fc_builder->xmax) ||
		 TransactionIdFollowsOrEquals(fc_xmax, fc_builder->xmax)))
	{
		fc_builder->xmax = fc_xmax;
		TransactionIdAdvance(fc_builder->xmax);
	}

	/* 如果有任何理由构建历史快照，立即执行 */
	if (fc_needs_snapshot)
	{
		/*
		 * 如果我们还没有构建完整的快照，就没有必要分发
		 * 它，反正也不会（也不能）使用。
		 */
		if (fc_builder->state < SNAPBUILD_FULL_SNAPSHOT)
			return;

		/*
		 * 减少快照生成器的旧快照引用计数，请注意
		 * 如果早前已将其分发到重排序缓冲区，它仍将被使用。
		 */
		if (fc_builder->snapshot)
			SnapBuildSnapDecRefcount(fc_builder->snapshot);

		fc_builder->snapshot = fc_SnapBuildBuildSnapshot(fc_builder);

		/* 我们可能需要执行无效化，添加快照 */
		if (!ReorderBufferXidHasBaseSnapshot(fc_builder->reorder, fc_xid))
		{
			fc_SnapBuildSnapIncRefcount(fc_builder->snapshot);
			ReorderBufferSetBaseSnapshot(fc_builder->reorder, fc_xid, fc_lsn,
										 fc_builder->snapshot);
		}

		/* 新快照的快照生成器引用计数 */
		fc_SnapBuildSnapIncRefcount(fc_builder->snapshot);

		/* 将一个新的目录快照添加到所有当前运行的事务中 */
		fc_SnapBuildDistributeNewCatalogSnapshot(fc_builder, fc_lsn);
	}
}


/* -----------------------------------
 * 处理 xlog 记录的快照构建函数
 * -----------------------------------
 */

/*
 * 处理正在运行的 xacts 记录，并使用其信息首先构建一个
 * 历史快照，然后释放不再需要的资源。
 */
void SnapBuildProcessRunningXacts(SnapBuild *fc_builder, XLogRecPtr fc_lsn, xl_running_xacts *fc_running)
{
	ReorderBufferTXN *fc_txn;
	TransactionId fc_xmin;

	/*
	 * 如果我们尚未保持一致，检查记录以查看它
	 * 是否允许更接近一致。如果我们一致，转储
	 * 我们的快照，以便其他人或我们在重新启动后可以使用它。
	 */
	if (fc_builder->state < SNAPBUILD_CONSISTENT)
	{
		/* 如果现在没有理由执行清理，则返回 false */
		if (!fc_SnapBuildFindSnapshot(fc_builder, fc_lsn, fc_running))
			return;
	}
	else
		fc_SnapBuildSerialize(fc_builder, fc_lsn);

	/*
	 * 根据正在运行的 xacts 信息更新有趣的 xids 范围。
	 * 我们不使用它来增加 ->xmax，因为一旦我们处于一致
	 * 的状态，我们可以自己做，并且效率更高，
	 * 因为我们只需要对目录事务执行此操作，
	 * 因为我们只查看这些。
	 *
	 * 注意：我们仅在修改目录的事务提交时增加 xmax
	 * （见 SnapBuildCommitTxn）。因此，xmax 可能低于
	 * xmin，这看起来很奇怪但实际上是正确的，且更有效，
	 * 因为我们在 heapam_visibility.c 中采用快速路径。
	 */
	fc_builder->xmin = fc_running->oldestRunningXid;

	/* 移除我们不需要再跟踪的事务 */
	fc_SnapBuildPurgeOlderTxn(fc_builder);

	/*
	 * 为当前复制槽提前xmin限制，以允许
	 * vacuum清理该槽所保护的元组。
	 *
	 * reorderbuffer可能在当前运行的
	 * 快照中有一个xmin；如果有，就使用它。如果没有，我们只需考虑
	 * 我们稍后将产生的快照，这不能小于
	 * 我们现在正在读取的记录中的最旧运行的xid。
	 */
	fc_xmin = ReorderBufferGetOldestXmin(fc_builder->reorder);
	if (fc_xmin == InvalidTransactionId)
		fc_xmin = fc_running->oldestRunningXid;
	elog(DEBUG3, "xmin: %u, xmax: %u, oldest running: %u, oldest xmin: %u",
		 fc_builder->xmin, fc_builder->xmax, fc_running->oldestRunningXid, fc_xmin);
	LogicalIncreaseXminForSlot(fc_lsn, fc_xmin);

	/*
	 * 还要告诉槽我们可以从哪里重新开始解码。我们不想在
	 * 每次提交后都这样做，因为改变这意味着对
	 * 逻辑槽状态文件的fsync，所以我们只在看到一个
	 * 正在运行的xacts记录时这样做。
	 *
	 * 通过查找正在进行的最早事务（由
	 * 相关记录中的第一个LSN确定）来做到这一点。每个事务
	 * 都记住了在开始之前我们最后一次将快照存储到磁盘的位置。
	 * 那一点是我们可以从中重新开始的地方。
	 */

	/*
	 * 如果我们不一致，就无法知道序列化快照的位置。
	 */
	if (fc_builder->state < SNAPBUILD_CONSISTENT)
		return;

	fc_txn = ReorderBufferGetOldestTXN(fc_builder->reorder);

	/*
	 * 最旧的正在进行的事务可能是在我们还未序列化
	 * 任何内容时开始的，因为我们还没有达到一致状态。
	 */
	if (fc_txn != NULL && fc_txn->restart_decoding_lsn != InvalidXLogRecPtr)
		LogicalIncreaseRestartDecodingForSlot(fc_lsn, fc_txn->restart_decoding_lsn);

	/*
	 * 没有进行中的事务，如果我们有一个，可以重用最后的序列化快照。
	 */
	else if (fc_txn == NULL &&
			 fc_builder->reorder->current_restart_decoding_lsn != InvalidXLogRecPtr &&
			 fc_builder->last_serialized_snapshot != InvalidXLogRecPtr)
		LogicalIncreaseRestartDecodingForSlot(fc_lsn,
											  fc_builder->last_serialized_snapshot);
}


/*
 * 构建一个能够解码目录的快照的开始。
 *
 * 在我们尚未一致时，为SnapBuildProcessRunningXacts()提供的辅助函数。
 *
 * 如果执行内部维护/清理使用xl_running_xacts记录是有意义的，则返回true。
 */
static bool fc_SnapBuildFindSnapshot(SnapBuild *fc_builder, XLogRecPtr fc_lsn, xl_running_xacts *fc_running)
{
	LogicalDecodingContext *fc_ctx = (LogicalDecodingContext *) fc_builder->reorder->private_data;

	/* ---
	 * 使用关于当前运行事务的信息逐步构建目录解码快照。有几种方法可以做到这一点：
	 *
	 * a) 在插入xl_running_xacts记录时没有运行的事务，立即跳到CONSISTENT。我们可能会在等待c)的子状态时找到这样的
	 *	  状态。
	 *
	 * b) 此（在先前的运行中）或另一个解码槽将快照序列化到磁盘中，我们可以使用。由于重新启动LSN将是一个
	 *	  任意的LSN，因此在查找解码更改的起始点时无法使用此方法，但我们需要找到起始点以提取更改，
	 *	  在这里我们不会看到部分事务的数据。此外，当槽需要完整快照以进行导出或直接使用时，
	 *	  由于该快照只包含目录修改事务，因此我们不能使用此方法。
	 *
	 * c) 首先逐步构建目录元组的快照
	 *	  （BUILDING_SNAPSHOT），这要求所有已在进行中的
	 *	  事务完成。然后，所有在此之后开始的事务
	 *	  （FULL_SNAPSHOT状态）都有足够的信息可以被解码。但是
	 *	  对于较旧的正在运行的事务，尚不存在可行的快照，
	 *	  因此只有在所有这些事务完成后才能达到CONSISTENT。
	 * ---
	 */

	/*
	 * xl_running_xact记录比我们可以使用的旧，我们可能没有
	 * 所有必要的目录行了。
	 */
	if (TransactionIdIsNormal(fc_builder->initial_xmin_horizon) &&
		NormalTransactionIdPrecedes(fc_running->oldestRunningXid,
									fc_builder->initial_xmin_horizon))
	{
		ereport(DEBUG1,
				(errmsg_internal("skipping snapshot at %X/%X while building logical decoding snapshot, xmin horizon too low",
								 LSN_FORMAT_ARGS(fc_lsn)),
				 errdetail_internal("initial xmin horizon of %u vs the snapshot's %u",
									fc_builder->initial_xmin_horizon, fc_running->oldestRunningXid)));


		fc_SnapBuildWaitSnapshot(fc_running, fc_builder->initial_xmin_horizon);

		return true;
	}

	/*
	 * a) 没有事务正在运行，我们可以跳到一致性状态。
	 *
	 * 这不受xl_running_xacts周围竞争的影响，因为我们可能会
	 * 漏掉事务提交，但当前没有事务开始。
	 *
	 * 注意：我们可能已经开始逐步组装快照，
	 * 所以我们需要小心处理这个问题。
	 */
	if (fc_running->oldestRunningXid == fc_running->nextXid)
	{
		if (fc_builder->start_decoding_at == InvalidXLogRecPtr ||
			fc_builder->start_decoding_at <= fc_lsn)
			/* 可以解码此后的所有内容 */
			fc_builder->start_decoding_at = fc_lsn + 1;

		/* 由于没有事务正在运行，xmin/xmax可以轻松设置。 */
		fc_builder->xmin = fc_running->nextXid;	/* < 已完成 */
		fc_builder->xmax = fc_running->nextXid;	/* >= 正在运行 */

		/* 所以我们可以安全地使用更快的比较 */
		Assert(TransactionIdIsNormal(fc_builder->xmin));
		Assert(TransactionIdIsNormal(fc_builder->xmax));

		fc_builder->state = SNAPBUILD_CONSISTENT;
		fc_builder->next_phase_at = InvalidTransactionId;

		ereport(LOG,
				(errmsg("logical decoding found consistent point at %X/%X",
						LSN_FORMAT_ARGS(fc_lsn)),
				 errdetail("There are no running transactions.")));

		return false;
	}

	/*
	 * b) 磁盘上的有效状态，并且在既不构建完整快照也不
	 * 创建插槽的情况下。
	 */
	else if (!fc_builder->building_full_snapshot &&
			 !fc_ctx->in_create &&
			 fc_SnapBuildRestore(fc_builder, fc_lsn))
	{
		int			fc_nxacts = fc_running->subxcnt + fc_running->xcnt;
		Size		fc_sz = sizeof(TransactionId) * fc_nxacts;

		/*
		 * 记住在xl_running_xacts记录写入时正在运行的事务和子事务
		 * 。我们稍后将使用此信息来识别已执行目录
		 * 更改的事务。请参见SnapBuildXidSetCatalogChanges。
		 */
		NInitialRunningXacts = fc_nxacts;
		InitialRunningXacts = MemoryContextAlloc(fc_builder->context, fc_sz);
		memcpy(InitialRunningXacts, fc_running->xids, fc_sz);
		qsort(InitialRunningXacts, fc_nxacts, sizeof(TransactionId), xidComparator);

		/* 不会有任何状态需要清理 */
		return false;
	}

	/*
	 * c) 从START过渡到BUILDING_SNAPSHOT。
	 *
	 * 在START状态，并且遇到一个xl_running_xacts记录，其中包含正在运行的xacts。
	 * 在这种情况下，切换到BUILDING_SNAPSHOT状态，并记录xl_running_xacts->nextXid。
	 * 一旦所有正在运行的xacts完成（即它们都是>= nextXid），
	 * 我们就拥有完整的目录快照。看起来我们可以使用xl_running_xact的->xids信息
	 * 更快地达到这一点，但这是有问题的，因为标记为运行的事务
	 * 可能已经插入了它们的提交记录 - 用锁定来改变这一点是不可行的。
	 */
	else if (fc_builder->state == SNAPBUILD_START)
	{
		fc_builder->state = SNAPBUILD_BUILDING_SNAPSHOT;
		fc_builder->next_phase_at = fc_running->nextXid;

		/*
		 * 从一个在所有当前运行的事务完成时对未来正确的xmin/xmax开始。
		 * 我们将在等待待处理事务完成时更新这两个值。
		 */
		fc_builder->xmin = fc_running->nextXid;	/* < 已完成 */
		fc_builder->xmax = fc_running->nextXid;	/* >= 正在运行 */

		/* 所以我们可以安全地使用更快的比较 */
		Assert(TransactionIdIsNormal(fc_builder->xmin));
		Assert(TransactionIdIsNormal(fc_builder->xmax));

		ereport(LOG,
				(errmsg("logical decoding found initial starting point at %X/%X",
						LSN_FORMAT_ARGS(fc_lsn)),
				 errdetail("Waiting for transactions (approximately %d) older than %u to end.",
						   fc_running->xcnt, fc_running->nextXid)));

		fc_SnapBuildWaitSnapshot(fc_running, fc_running->nextXid);
	}

	/*
	 * c) 从BUILDING_SNAPSHOT过渡到FULL_SNAPSHOT。
	 *
	 * 在BUILDING_SNAPSHOT状态，并且这个xl_running_xacts的oldestRunningXid
	 * >= 在我们切换到BUILDING_SNAPSHOT时的nextXid。这
	 * 意味着所有随后开始的事务都有足够的信息
	 * 可以解码。切换到FULL_SNAPSHOT。
	 */
	else if (fc_builder->state == SNAPBUILD_BUILDING_SNAPSHOT &&
			 TransactionIdPrecedesOrEquals(fc_builder->next_phase_at,
										   fc_running->oldestRunningXid))
	{
		fc_builder->state = SNAPBUILD_FULL_SNAPSHOT;
		fc_builder->next_phase_at = fc_running->nextXid;

		ereport(LOG,
				(errmsg("logical decoding found initial consistent point at %X/%X",
						LSN_FORMAT_ARGS(fc_lsn)),
				 errdetail("Waiting for transactions (approximately %d) older than %u to end.",
						   fc_running->xcnt, fc_running->nextXid)));

		fc_SnapBuildWaitSnapshot(fc_running, fc_running->nextXid);
	}

	/*
	 * c) 从FULL_SNAPSHOT过渡到CONSISTENT。
	 *
	 * 在FULL_SNAPSHOT状态（见d），并且这个xl_running_xacts的
	 * oldestRunningXid >= 在我们切换到
	 * FULL_SNAPSHOT时的nextXid。这
	 * 意味着所有当前正在进行的事务都有一个目录快照，
	 * 并且它们的所有更改已被收集。切换到CONSISTENT。
	 */
	else if (fc_builder->state == SNAPBUILD_FULL_SNAPSHOT &&
			 TransactionIdPrecedesOrEquals(fc_builder->next_phase_at,
										   fc_running->oldestRunningXid))
	{
		fc_builder->state = SNAPBUILD_CONSISTENT;
		fc_builder->next_phase_at = InvalidTransactionId;

		ereport(LOG,
				(errmsg("logical decoding found consistent point at %X/%X",
						LSN_FORMAT_ARGS(fc_lsn)),
				 errdetail("There are no old transactions anymore.")));
	}

	/*
	 * 我们已经开始跟踪正在运行的xacts，需要等待所有
	 * 正在进行的事务完成。我们进入正常的记录处理，因此
	 * 可以进行增量清理。
	 */
	return true;
}

/* ---
 * 迭代记录中的 xids，等待所有比截止日期早的 xids 完成。
 * 然后，如果可能，记录一个新的 xl_running_xacts 记录。
 *
 * 这对于解码的正确性不是必需的，但为了：
 * a) 允许 isolationtester 注意到我们当前正在等待某些事情。
 * b) 在有帮助的地方记录一个新的 xl_running_xacts 记录，而无需
 *	  等待 bgwriter 或 checkpointer。
 * ---
 */
static void fc_SnapBuildWaitSnapshot(xl_running_xacts *fc_running, TransactionId fc_cutoff)
{
	int			fc_off;

	for (fc_off = 0; fc_off < fc_running->xcnt; fc_off++)
	{
		TransactionId fc_xid = fc_running->xids[fc_off];

		/*
		 * 上层应该防止我们需要在自己身上等待。
		 * 再检查一遍，因为不这样做可能会导致
		 * 无休止的等待或 Assert() 失败。
		 */
		if (TransactionIdIsCurrentTransactionId(fc_xid))
			elog(ERROR, "waiting for ourselves");

		if (TransactionIdFollows(fc_xid, fc_cutoff))
			continue;

		XactLockTableWait(fc_xid, NULL, NULL, XLTW_None);
	}

	/*
	 * 所有我们需要完成的事务都已完成 - 尝试确保及时有
	 * 另一个 xl_running_xacts 记录，而无需等待
	 * bgwriter 或 checkpointer 记录一个。 在恢复期间我们无法
	 * 强制执行这一点，所以我们必须等待。
	 */
	if (!RecoveryInProgress())
	{
		LogStandbySnapshot();
	}
}

/* -----------------------------------
 * 快照序列化支持
 * -----------------------------------
 */

/*
 * 我们以以下方式将 struct SnapBuild 的当前状态存储在磁盘上：
 *
 * struct SnapBuildOnDisk;
 * TransactionId * committed.xcnt; (*不是 xcnt_space*)
 *
 */
typedef struct SnapBuildOnDisk
{
	/* 该结构的第一部分需要与版本无关 */

	/* 不受校验和覆盖的数据 */
	uint32		magic;
	pg_crc32c	checksum;

	/* 受校验和覆盖的数据 */

	/* 版本，以防我们想支持 pg_upgrade */
	uint32		version;
	/* 磁盘数据的大小，不包括常量大小部分 */
	uint32		length;

	/* 版本依赖部分 */
	SnapBuild	builder;

	/* 变量数量的 TransactionIds 随后跟随 */
} SnapBuildOnDisk;

#define SnapBuildOnDiskConstantSize \
	offsetof(SnapBuildOnDisk, builder)
#define SnapBuildOnDiskNotChecksummedSize \
	offsetof(SnapBuildOnDisk, version)

#define SNAPBUILD_MAGIC 0x51A1E001
#define SNAPBUILD_VERSION 4

/*
 * 根据快照构建器的状态从磁盘存储/加载快照。
 *
 * 应该由不属于 snapbuild.c 的外部代码使用，该代码只是读取
 * 可能是序列化快照的记录的位置。
 */
void SnapBuildSerializationPoint(SnapBuild *fc_builder, XLogRecPtr fc_lsn)
{
	if (fc_builder->state < SNAPBUILD_CONSISTENT)
		fc_SnapBuildRestore(fc_builder, fc_lsn);
	else
		fc_SnapBuildSerialize(fc_builder, fc_lsn);
}

/*
 * 如果其他解码进程尚未在 'lsn' 位置序列化快照 'builder'，
 * 则序列化快照 'builder'。
 */
static void fc_SnapBuildSerialize(SnapBuild *fc_builder, XLogRecPtr fc_lsn)
{
	Size		fc_needed_length;
	SnapBuildOnDisk *fc_ondisk = NULL;
	char	   *fc_ondisk_c;
	int			fc_fd;
	char		fc_tmppath[MAXPGPATH];
	char		fc_path[MAXPGPATH];
	int			fc_ret;
	struct stat fc_stat_buf;
	Size		fc_sz;

	Assert(fc_lsn != InvalidXLogRecPtr);
	Assert(fc_builder->last_serialized_snapshot == InvalidXLogRecPtr ||
		   fc_builder->last_serialized_snapshot <= fc_lsn);

	/*
	 * 如果我们无法在恢复快照后立即继续工作，
	 * 则序列化没有意义
	 */
	if (fc_builder->state < SNAPBUILD_CONSISTENT)
		return;

	/* 一致的快照没有下一个阶段 */
	Assert(fc_builder->next_phase_at == InvalidTransactionId);

	/*
	 * 我们通过它们有效的 LSN 来识别快照。 我们不需要在名称中
	 * 包括时间线，因为每个 LSN 映射到恰好一个时间线，
	 * 除非用户使用 pg_resetwal 或类似工具。 如果用户这样做了，
	 * 无论如何都没有希望继续解码。
	 */
	sprintf(fc_path, "pg_logical/snapshots/%X-%X.snap",
			LSN_FORMAT_ARGS(fc_lsn));

	/*
	 * 首先检查其他后端是否已经为此 LSN 写入了快照。
	 * 如果没有，完全没问题，因此我们将 ENOENT 视为有效状态。
	 * 其他所有情况都是意外错误。
	 */
	fc_ret = stat(fc_path, &fc_stat_buf);

	if (fc_ret != 0 && errno != ENOENT)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not stat file \"%s\": %m", fc_path)));

	else if (fc_ret == 0)
	{
		/*
		 * 其他人已经序列化到这一点，不要覆盖
		 * 但记住位置，这样我们就不需要再次读取旧数据。
		 *
		 * 为了确保在从临时文件名重命名到实际文件名后已同步到磁盘，
		 * 我们只是重复 fsync。 这应该是便宜的，因为在大多数场景中，它应该
		 * 已安全地在磁盘上。
		 */
		fsync_fname(fc_path, false);
		fsync_fname("pg_logical/snapshots", true);

		fc_builder->last_serialized_snapshot = fc_lsn;
		goto out;
	}

	/*
	 * 在我们 stat(2) 文件和写入文件之间显然存在一个竞争条件。但我们将文件原子性地重命名到指定位置，所有创建的文件本来就需要包含相同的数据，因此这完全没问题，尽管有点资源浪费。锁定似乎是毫无意义的复杂化。
	 */
	elog(DEBUG1, "serializing snapshot to %s", fc_path);

	/* 为了确保只有我们会写入此临时文件，包含 pid */
	sprintf(fc_tmppath, "pg_logical/snapshots/%X-%X.snap.%d.tmp",
			LSN_FORMAT_ARGS(fc_lsn), MyProcPid);

	/*
	 * 如果临时文件已经存在，则 unlink 它，需要在崩溃/错误之前进行，因为我们不会在单个解码插槽/后端中两次进入此函数，并且临时文件包含当前进程的 pid。
	 */
	if (unlink(fc_tmppath) != 0 && errno != ENOENT)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not remove file \"%s\": %m", fc_tmppath)));

	fc_needed_length = sizeof(SnapBuildOnDisk) +
		sizeof(TransactionId) * fc_builder->committed.xcnt;

	fc_ondisk_c = MemoryContextAllocZero(fc_builder->context, fc_needed_length);
	fc_ondisk = (SnapBuildOnDisk *) fc_ondisk_c;
	fc_ondisk->magic = SNAPBUILD_MAGIC;
	fc_ondisk->version = SNAPBUILD_VERSION;
	fc_ondisk->length = fc_needed_length;
	INIT_CRC32C(fc_ondisk->checksum);
	COMP_CRC32C(fc_ondisk->checksum,
				((char *) fc_ondisk) + SnapBuildOnDiskNotChecksummedSize,
				SnapBuildOnDiskConstantSize - SnapBuildOnDiskNotChecksummedSize);
	fc_ondisk_c += sizeof(SnapBuildOnDisk);

	memcpy(&fc_ondisk->builder, fc_builder, sizeof(SnapBuild));
	/* 将内存仅数据置为 NULL */
	fc_ondisk->builder.context = NULL;
	fc_ondisk->builder.snapshot = NULL;
	fc_ondisk->builder.reorder = NULL;
	fc_ondisk->builder.committed.xip = NULL;

	COMP_CRC32C(fc_ondisk->checksum,
				&fc_ondisk->builder,
				sizeof(SnapBuild));

	/* 复制已提交的 xacts */
	fc_sz = sizeof(TransactionId) * fc_builder->committed.xcnt;
	memcpy(fc_ondisk_c, fc_builder->committed.xip, fc_sz);
	COMP_CRC32C(fc_ondisk->checksum, fc_ondisk_c, fc_sz);
	fc_ondisk_c += fc_sz;

	FIN_CRC32C(fc_ondisk->checksum);

	/* 我们现在有有效的数据，打开临时文件并写入其中 */
	fc_fd = OpenTransientFile(fc_tmppath,
						   O_CREAT | O_EXCL | O_WRONLY | PG_BINARY);
	if (fc_fd < 0)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not open file \"%s\": %m", fc_tmppath)));

	errno = 0;
	pgstat_report_wait_start(WAIT_EVENT_SNAPBUILD_WRITE);
	if ((write(fc_fd, fc_ondisk, fc_needed_length)) != fc_needed_length)
	{
		int			fc_save_errno = errno;

		CloseTransientFile(fc_fd);

		/* 如果写入未设置 errno，假设问题是没有磁盘空间 */
		errno = fc_save_errno ? fc_save_errno : ENOSPC;
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not write to file \"%s\": %m", fc_tmppath)));
	}
	pgstat_report_wait_end();

	/*
	 * 在重命名之前 fsync 文件，以便即使在此之后崩溃，我们也有一个完全有效的文件或什么都没有。
	 *
	 * 在这里仅仅在 fsync() 时报告错误是安全的，因为我们会重试整个操作，包括写入。
	 *
	 * TODO: 通过检查点/重启点执行 fsync()，因为在解码过程中这样做会有一些显著的开销。
	 */
	pgstat_report_wait_start(WAIT_EVENT_SNAPBUILD_SYNC);
	if (pg_fsync(fc_fd) != 0)
	{
		int			fc_save_errno = errno;

		CloseTransientFile(fc_fd);
		errno = fc_save_errno;
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not fsync file \"%s\": %m", fc_tmppath)));
	}
	pgstat_report_wait_end();

	if (CloseTransientFile(fc_fd) != 0)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not close file \"%s\": %m", fc_tmppath)));

	fsync_fname("pg_logical/snapshots", true);

	/*
	 * 我们可能会覆盖某个其他后端的工作，但这没关系，我们的快照也是有效的，我们只是做了一些多余的工作。
	 */
	if (rename(fc_tmppath, fc_path) != 0)
	{
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not rename file \"%s\" to \"%s\": %m",
						fc_tmppath, fc_path)));
	}

	/* 确保我们持久化 */
	fsync_fname(fc_path, false);
	fsync_fname("pg_logical/snapshots", true);

	/*
	 * 现在我们再也无法丢失已转储的状态，请记住这一点作为序列化点。
	 */
	fc_builder->last_serialized_snapshot = fc_lsn;

out:
	ReorderBufferSetRestartPoint(fc_builder->reorder,
								 fc_builder->last_serialized_snapshot);
	/* 保持整洁 */
	if (fc_ondisk)
		pfree(fc_ondisk);
}

/*
 * 如果之前在 'lsn' 指示的位置存储了快照，则将其恢复到 'builder' 中。
 * 如果成功则返回 true，否则返回 false。
 */
static bool fc_SnapBuildRestore(SnapBuild *fc_builder, XLogRecPtr fc_lsn)
{
	SnapBuildOnDisk fc_ondisk;
	int			fc_fd;
	char		fc_path[MAXPGPATH];
	Size		fc_sz;
	int			fc_readBytes;
	pg_crc32c	fc_checksum;

	/* 如果我们已经在那，就没有必要加载快照 */
	if (fc_builder->state == SNAPBUILD_CONSISTENT)
		return false;

	sprintf(fc_path, "pg_logical/snapshots/%X-%X.snap",
			LSN_FORMAT_ARGS(fc_lsn));

	fc_fd = OpenTransientFile(fc_path, O_RDONLY | PG_BINARY);

	if (fc_fd < 0 && errno == ENOENT)
		return false;
	else if (fc_fd < 0)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not open file \"%s\": %m", fc_path)));

	/* ----
	 * 确保快照已安全保存到磁盘，通常这很简单。
	 * 请注意，我们在这里不需要 PANIC，没人能够在不 fsync 的情况下使用插槽，并且在没有 fsync() 的情况下保存也不会成功...
	 * ----
	 */
	fsync_fname(fc_path, false);
	fsync_fname("pg_logical/snapshots", true);


	/* 读取静态大小的快照部分 */
	pgstat_report_wait_start(WAIT_EVENT_SNAPBUILD_READ);
	fc_readBytes = read(fc_fd, &fc_ondisk, SnapBuildOnDiskConstantSize);
	pgstat_report_wait_end();
	if (fc_readBytes != SnapBuildOnDiskConstantSize)
	{
		int			fc_save_errno = errno;

		CloseTransientFile(fc_fd);

		if (fc_readBytes < 0)
		{
			errno = fc_save_errno;
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not read file \"%s\": %m", fc_path)));
		}
		else
			ereport(ERROR,
					(errcode(ERRCODE_DATA_CORRUPTED),
					 errmsg("could not read file \"%s\": read %d of %zu",
							fc_path, fc_readBytes,
							(Size) SnapBuildOnDiskConstantSize)));
	}

	if (fc_ondisk.magic != SNAPBUILD_MAGIC)
		ereport(ERROR,
				(errcode(ERRCODE_DATA_CORRUPTED),
				 errmsg("snapbuild state file \"%s\" has wrong magic number: %u instead of %u",
						fc_path, fc_ondisk.magic, SNAPBUILD_MAGIC)));

	if (fc_ondisk.version != SNAPBUILD_VERSION)
		ereport(ERROR,
				(errcode(ERRCODE_DATA_CORRUPTED),
				 errmsg("snapbuild state file \"%s\" has unsupported version: %u instead of %u",
						fc_path, fc_ondisk.version, SNAPBUILD_VERSION)));

	INIT_CRC32C(fc_checksum);
	COMP_CRC32C(fc_checksum,
				((char *) &fc_ondisk) + SnapBuildOnDiskNotChecksummedSize,
				SnapBuildOnDiskConstantSize - SnapBuildOnDiskNotChecksummedSize);

	/* 读取 SnapBuild */
	pgstat_report_wait_start(WAIT_EVENT_SNAPBUILD_READ);
	fc_readBytes = read(fc_fd, &fc_ondisk.builder, sizeof(SnapBuild));
	pgstat_report_wait_end();
	if (fc_readBytes != sizeof(SnapBuild))
	{
		int			fc_save_errno = errno;

		CloseTransientFile(fc_fd);

		if (fc_readBytes < 0)
		{
			errno = fc_save_errno;
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not read file \"%s\": %m", fc_path)));
		}
		else
			ereport(ERROR,
					(errcode(ERRCODE_DATA_CORRUPTED),
					 errmsg("could not read file \"%s\": read %d of %zu",
							fc_path, fc_readBytes, sizeof(SnapBuild))));
	}
	COMP_CRC32C(fc_checksum, &fc_ondisk.builder, sizeof(SnapBuild));

	/* 恢复已提交的 xacts 信息 */
	fc_sz = sizeof(TransactionId) * fc_ondisk.builder.committed.xcnt;
	fc_ondisk.builder.committed.xip = MemoryContextAllocZero(fc_builder->context, fc_sz);
	pgstat_report_wait_start(WAIT_EVENT_SNAPBUILD_READ);
	fc_readBytes = read(fc_fd, fc_ondisk.builder.committed.xip, fc_sz);
	pgstat_report_wait_end();
	if (fc_readBytes != fc_sz)
	{
		int			fc_save_errno = errno;

		CloseTransientFile(fc_fd);

		if (fc_readBytes < 0)
		{
			errno = fc_save_errno;
			ereport(ERROR,
					(errcode_for_file_access(),
					 errmsg("could not read file \"%s\": %m", fc_path)));
		}
		else
			ereport(ERROR,
					(errcode(ERRCODE_DATA_CORRUPTED),
					 errmsg("could not read file \"%s\": read %d of %zu",
							fc_path, fc_readBytes, fc_sz)));
	}
	COMP_CRC32C(fc_checksum, fc_ondisk.builder.committed.xip, fc_sz);

	if (CloseTransientFile(fc_fd) != 0)
		ereport(ERROR,
				(errcode_for_file_access(),
				 errmsg("could not close file \"%s\": %m", fc_path)));

	FIN_CRC32C(fc_checksum);

	/* 验证我们读取内容的校验和 */
	if (!EQ_CRC32C(fc_checksum, fc_ondisk.checksum))
		ereport(ERROR,
				(errcode(ERRCODE_DATA_CORRUPTED),
				 errmsg("checksum mismatch for snapbuild state file \"%s\": is %u, should be %u",
						fc_path, fc_checksum, fc_ondisk.checksum)));

	/*
	 * 好吧，我们现在在这里有一个合理的快照，弄清楚它是否比我们已有的更多信息。
	 */

	/*
	 * 目前我们仅对一致的快照感兴趣，比较一个不完整的快照是否更“先进”似乎是不必要复杂的。
	 */
	if (fc_ondisk.builder.state < SNAPBUILD_CONSISTENT)
		goto snapshot_not_interesting;

	/*
	 * 不要使用一个需要我们无法保证可用的 xmin 的快照。
	 */
	if (TransactionIdPrecedes(fc_ondisk.builder.xmin, fc_builder->initial_xmin_horizon))
		goto snapshot_not_interesting;

	/* 一致的快照没有下一个阶段 */
	Assert(fc_ondisk.builder.next_phase_at == InvalidTransactionId);

	/* 好吧，我们认为快照是合理的，复制所有重要内容 */
	fc_builder->xmin = fc_ondisk.builder.xmin;
	fc_builder->xmax = fc_ondisk.builder.xmax;
	fc_builder->state = fc_ondisk.builder.state;

	fc_builder->committed.xcnt = fc_ondisk.builder.committed.xcnt;
	/* 我们只分配/存储了 xcnt，而不是 xcnt_space xids ! */
	/* 如果我们这里没有任何内容，请不要覆盖预分配的 xip */
	if (fc_builder->committed.xcnt > 0)
	{
		pfree(fc_builder->committed.xip);
		fc_builder->committed.xcnt_space = fc_ondisk.builder.committed.xcnt;
		fc_builder->committed.xip = fc_ondisk.builder.committed.xip;
	}
	fc_ondisk.builder.committed.xip = NULL;

	/* 我们的快照已经不再有趣，建立一个新的 */
	if (fc_builder->snapshot != NULL)
	{
		SnapBuildSnapDecRefcount(fc_builder->snapshot);
	}
	fc_builder->snapshot = fc_SnapBuildBuildSnapshot(fc_builder);
	fc_SnapBuildSnapIncRefcount(fc_builder->snapshot);

	ReorderBufferSetRestartPoint(fc_builder->reorder, fc_lsn);

	Assert(fc_builder->state == SNAPBUILD_CONSISTENT);

	ereport(LOG,
			(errmsg("logical decoding found consistent point at %X/%X",
					LSN_FORMAT_ARGS(fc_lsn)),
			 errdetail("Logical decoding will begin using saved snapshot.")));
	return true;

snapshot_not_interesting:
	if (fc_ondisk.builder.committed.xip != NULL)
		pfree(fc_ondisk.builder.committed.xip);
	return false;
}

/*
 * 删除所有不再需要的序列化快照，因为没有槽可以需要它们。这实际上不必在检查点期间运行，但这是一个便于调度的时机。
 *
 * 注意：即使逻辑解码已禁用，我们仍在检查点期间运行此操作，因此我们会在禁用后某个时间清理旧槽。
 */
void CheckPointSnapBuild(void)
{
	XLogRecPtr	fc_cutoff;
	XLogRecPtr	fc_redo;
	DIR		   *fc_snap_dir;
	struct dirent *fc_snap_de;
	char		fc_path[MAXPGPATH + 21];

	/*
	 * 我们以最后的重做指针的最小值开始。没有新的复制槽会在此之前启动，因此这是一个安全的上限以进行移除。
	 */
	fc_redo = GetRedoRecPtr();

	/* 现在检查现有插槽的重启指针 */
	fc_cutoff = ReplicationSlotsComputeLogicalRestartLSN();

	/* 不要早于重启 lsn 开始 */
	if (fc_redo < fc_cutoff)
		fc_cutoff = fc_redo;

	fc_snap_dir = AllocateDir("pg_logical/snapshots");
	while ((fc_snap_de = ReadDir(fc_snap_dir, "pg_logical/snapshots")) != NULL)
	{
		uint32		fc_hi;
		uint32		fc_lo;
		XLogRecPtr	fc_lsn;
		struct stat fc_statbuf;

		if (strcmp(fc_snap_de->d_name, ".") == 0 ||
			strcmp(fc_snap_de->d_name, "..") == 0)
			continue;

		snprintf(fc_path, sizeof(fc_path), "pg_logical/snapshots/%s", fc_snap_de->d_name);

		if (lstat(fc_path, &fc_statbuf) == 0 && !S_ISREG(fc_statbuf.st_mode))
		{
			elog(DEBUG1, "only regular files expected: %s", fc_path);
			continue;
		}

		/*
		 * 来自 SnapBuildSerialize() 的临时文件名包括 LSN 和所有内容，但后缀为 .$pid.tmp。我们可以像处理其他文件一样删除它们，因为没有任何文件在截止日期之前被写入。
		 *
		 * 如果文件不符合模式，我们只记录一条消息，这可能是一些编辑器的锁定/状态文件或类似的东西...
		 */
		if (sscanf(fc_snap_de->d_name, "%X-%X.snap", &fc_hi, &fc_lo) != 2)
		{
			ereport(LOG,
					(errmsg("could not parse file name \"%s\"", fc_path)));
			continue;
		}

		fc_lsn = ((uint64) fc_hi) << 32 | fc_lo;

		/* 检查我们是否仍然需要它 */
		if (fc_lsn < fc_cutoff || fc_cutoff == InvalidXLogRecPtr)
		{
			elog(DEBUG1, "removing snapbuild snapshot %s", fc_path);

			/*
			 * 如果我们不能在这里删除文件，虽然奇怪，但并不是特别有害。不要阻止检查点的完成，那将是治病无药的。
			 */
			if (unlink(fc_path) < 0)
			{
				ereport(LOG,
						(errcode_for_file_access(),
						 errmsg("could not remove file \"%s\": %m",
								fc_path)));
				continue;
			}
		}
	}
	FreeDir(fc_snap_dir);
}

/*
 * 如果给定的 xid 在初始运行事务的列表中，我们标记该事务及其子事务包含目录更改。有关更多信息，请参见 NInitialRunningXacts 和 InitialRunningXacts 的注释。
 */
void SnapBuildXidSetCatalogChanges(SnapBuild *fc_builder, TransactionId fc_xid, int fc_subxcnt,
							  TransactionId *fc_subxacts, XLogRecPtr fc_lsn)
{
	/*
	 * 如果没有初始运行事务的信息，则跳过。
	 *
	 * 即使该事务已标记为包含目录更改，也无法跳过，因为可能未标记修改目录的子事务。
	 */
	if (NInitialRunningXacts == 0)
		return;

	if (bsearch(&fc_xid, InitialRunningXacts, NInitialRunningXacts,
				sizeof(TransactionId), xidComparator) != NULL)
	{
		ReorderBufferXidSetCatalogChanges(fc_builder->reorder, fc_xid, fc_lsn);

		for (int fc_i = 0; fc_i < fc_subxcnt; fc_i++)
			ReorderBufferXidSetCatalogChanges(fc_builder->reorder, fc_subxacts[fc_i], fc_lsn);
	}
}
